<!--
    by: yuanbiao 2021年05月17日
    name: x-video-record
    notes:
-->
<template>
  <div ref="xVideoRecord" :class="[`${prefixCls}`]"
    @keydown.prevent.stop="keydownHandler"
    tabindex="1"
  >
    <div ref="videoWrapper" :class="`${prefixCls}__main`">
      <video ref="video" :class="`${prefixCls}__video`"
        :src="videoSrc"
        :style="videoStyle"
        :x5-playsinline="true" :playsinline="true" :webkit-playsinline="true"
        :controls="false" autoplay
        @play="playCallback"
        @canplay="canplayCallback"
        @error="errorCallback"
      ></video>
    </div>
    <div :class="`${prefixCls}__mask`">
      <i v-show="isSaving" class="xvu-iconfont xvu-spinner"></i>
    </div>
    <div :class="`${prefixCls}__toolsbar`">
      <div :class="`${prefixCls}__toolsbar_left`">
        <div :class="`${prefixCls}__toolsbar_btn`" @click="makeScale(20)">
          <div :class="[`${prefixCls}__toolsbar_btn_icon`, 'xvu-iconfont', 'xvu-zoomin']"></div>
          <div :class="[`${prefixCls}__toolsbar_btn_label`]">放大</div>
        </div>
        <div :class="`${prefixCls}__toolsbar_btn`" @click="makeScale(-20)">
          <div :class="[`${prefixCls}__toolsbar_btn_icon`, 'xvu-iconfont', 'xvu-zoomout']"></div>
          <div :class="[`${prefixCls}__toolsbar_btn_label`]">缩小</div>
        </div>
        <div v-if="mediaDevicesList.length === 0" :class="`${prefixCls}__toolsbar_btn`" @click="checkCamera">
          <div :class="[`${prefixCls}__toolsbar_btn_icon`, 'xvu-iconfont', 'xvu-check']"></div>
          <div :class="[`${prefixCls}__toolsbar_btn_label`]">检测镜头</div>
        </div>
        <div v-else-if="mediaDevicesList.length > 1" :class="`${prefixCls}__toolsbar_btn`" @click="switchCamera">
          <div :class="[`${prefixCls}__toolsbar_btn_icon`, 'xvu-iconfont', 'xvu-photo-switch']"></div>
          <div :class="[`${prefixCls}__toolsbar_btn_label`]">切换镜头</div>
        </div>
      </div>
      <div :class="`${prefixCls}__toolsbar_middle`">
        <div :class="`${prefixCls}__toolsbar_btn`" @click="makePhoto">
          <div :class="[`${prefixCls}__toolsbar_btn_onlyicon`, 'xvu-iconfont', 'xvu-photo-fill']"></div>
        </div>
        <div :class="`${prefixCls}__toolsbar_btn`" @click="makeShoot">
          <div v-if="!isShooting" :class="[`${prefixCls}__toolsbar_btn_onlyicon`, 'xvu-iconfont', 'xvu-camera-fill']"></div>
          <div v-else :class="[`${prefixCls}__toolsbar_btn_onlyicon`, 'xvu-iconfont', 'xvu-stop-fill']">
            <div :class="`${prefixCls}__toolsbar_btn_shoottime`">
              {{recordDuration | formatTime}}
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Toast from '@/components/toast/index.js'
const prefixCls = 'x-video-record'
export default {
  name: 'x-video-record',
  props: {
    options: {
      type: Object,
      default: () => {
        // 音频比特率128kbps，视频比特率2.5Mbps
        return {
          dataType : 'base64',
          audioBitsPerSecond : 128000,
          videoBitsPerSecond : 2500000,
          mimeType : 'video/webm'
        }
      }
    },
    cameraId: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      prefixCls: prefixCls,
      videoSrc: '',
      videoRecorder: null,
      videoChunks: [],
      mediaDevicesList: [],
      mediaDeviceActiveIndex: 0,
      mediaStream: null,
      deviceId: '',
      constraints: { video: {}, audio: true },
      scale: 100,         // 缩放百分比分子
      isSaving: false,    // 正在保存
      isShooting: false,  // 正在摄像
      canMakePhoto: true, // 是否可拍照
      recordTimer: null,  // 摄像计时器
      recordDuration: 0,  // 摄像进度
      error: false,       // 直播出错
    }
  },
  computed: {
    videoStyle () {
      return {
        width: `${this.scale}%`,
        height: `${this.scale}%`
      }
    }
  },
  filters: {
    // 秒转时分秒
    formatTime (time) {
      let h = Math.floor(time / 3600) < 10 ? '0' + Math.floor(time / 3600) : Math.floor(time / 3600)
      let m = Math.floor((time / 60 % 60)) < 10 ? '0' + Math.floor((time / 60 % 60)) : Math.floor((time / 60 % 60))
      let s = Math.floor((time % 60)) < 10 ? '0' + Math.floor((time % 60)) : Math.floor((time % 60))
      time = (isNaN(h) || isNaN(m) || isNaN(s)) ? `--:--:--` : `${h}:${m}:${s}`
      return time
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.init()
    })
  },
  beforeDestroy () {
    this.resetData()
  },
  methods: {
    init () {
      this.checkCamera()
    },
    // 键盘事件
    keydownHandler (e) {
      let keyCode = e.keyCode || e.which || e.charCode
      if (!this.$refs.video) return
      if (keyCode === 32 && this.isShooting) {
        // 空格
        this.makeShoot()
      }
    },
    // 核实是否有摄像头
    checkCamera () {
      if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
        // 未知则直接交给getCamera做尝试
        this.getCamera('', true)
      } else {
        navigator.mediaDevices.enumerateDevices().then((deviceInfos) => {
          this.mediaDevicesList = deviceInfos.filter(v => v.kind === 'videoinput')
          if (this.mediaDevicesList.length) {
            this.mediaDeviceActiveIndex = 0
            this.cameraId && this.mediaDevicesList.forEach((v, i) => {
              if (this.cameraId === v.deviceId) {
                this.mediaDeviceActiveIndex = i
              }
            })
            this.getCamera((this.mediaDevicesList[this.mediaDeviceActiveIndex] || {}).deviceId || '', true)
          } else {
            Toast('未检测到可用的摄像头设备，请连接设备后重试')
          }
        })
      }
    },
    // 调用摄像头
    getCamera (deviceId, audioFlag) {
      this.closeCamera()
      this.constraints = {
        video: deviceId ? { deviceId: { exact: deviceId }} : {},
        audio: audioFlag
      }
      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia(this.constraints).then(this.getCameraSuccess).catch(this.getCameraError)
      } else if (navigator.webkitGetUserMedia) {
        navigator.webkitGetUserMedia(this.constraints).then(this.getCameraSuccess).catch(this.getCameraError)
      } else if (navigator.mozGetUserMedia) {
        navigator.mozGetUserMedia(this.constraints).then(this.getCameraSuccess).catch(this.getCameraError)
      } else if (navigator.msGetUserMedia) {
        navigator.msGetUserMedia(this.constraints).then(this.getCameraSuccess).catch(this.getCameraError)
      } else if (navigator.getUserMedia) {
        navigator.getUserMedia(this.constraints).then(this.getCameraSuccess).catch(this.getCameraError)
      } else {
        Toast('你的浏览器不支持访问用户媒体设备，请更换浏览器后重试')
      }
    },
    // 调用摄像头 - 成功
    getCameraSuccess (stream) {
      this.deviceId = (this.mediaDevicesList[this.mediaDeviceActiveIndex] || {}).deviceId
      this.mediaStream = stream
      let _this = this
      if (!this.constraints.audio) {
        Toast('录音设备不可用，录像可能没有声音')
      }
      try {
        _this.videoSrc = (window.URL || window.webkitURL).createObjectURL(stream)
      } catch (err) {
        _this.$refs.video && (_this.$refs.video.srcObject = stream)
      }
      this.initVideoRecorder()
    },
    // 调用摄像头 - 失败
    getCameraError (err) {
      if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
        Toast('未检测到可用的摄像头设备，请连接设备后重试')
      } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError' || err.name === 'SourceUnavailableError') {
        Toast('检测到设备正被占用，请稍后重试')
      } else if (err.name === 'OverconstrainedError' || err.name === 'ConstraintNotSatisfiedError') {
        Toast('检测到设备参数无法满足功能需求')
      } else if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
        Toast('调用设备权限被拒绝')
      } else if (err.name === 'TypeError') {
        Toast('设备参数要求未设置')
      } else {
        Toast('访问用户媒体设备失败：', err.name, err.message)
      }
      if (this.constraints.audio) {
        this.getCamera(this.deviceId, false)
      }
    },
    // 多个摄像头之间切换
    switchCamera () {
      if (!this.mediaDevicesList.length) return
      this.mediaDeviceActiveIndex = (this.mediaDeviceActiveIndex + 1) % this.mediaDevicesList.length
      this.getCamera((this.mediaDevicesList[this.mediaDeviceActiveIndex] || {}).deviceId, true)
    },
    // 关闭摄像头
    closeCamera () {
      if (this.mediaStream) {
        if (this.mediaStream.stop) {
          this.mediaStream.stop()
        } else {
          // Chrome 45版本以上关闭摄像头的方式
          let track = this.mediaStream.getTracks()[0]
          track.stop && track.stop()
        }
      }
    },
    videoChunks2DataUrl (videoChunks) {
      return new Promise((resolve, reject) => {
        let blob = new Blob(videoChunks, {type: this.options.mimeType || 'video/webm'})
        let fileReader = new FileReader()
        fileReader.readAsDataURL(blob)
        fileReader.onloadend = (data) => { resolve(data) }
        fileReader.onerror = (err) => { reject(err) }
      })
    },
    // 初始化MediaRecorder
    initVideoRecorder () {
      this.videoRecorder = new MediaRecorder(this.mediaStream, {
        audioBitsPerSecond : this.options.audioBitsPerSecond || 128000,
        videoBitsPerSecond : this.options.videoBitsPerSecond || 2500000,
        mimeType : this.options.mimeType || 'video/webm'
      })
      // 开始
      this.videoRecorder.onstart = (e) => {
        console.log('videoRecorder onstart:', e)
        // 每次开始录制时，清空chunks
        this.videoChunks = []
      }
      // 每次记录一定时间的数据时，会定期触发。没有指定时间片，则记录整个数据时触发
      this.videoRecorder.ondataavailable = (e) => {
        console.log('videoRecorder ondataavailable:', e)
        // 记录视频片段
        if (e.data && e.data.size) {
          this.videoChunks.push(e.data)
          // 分片段输出（适用于录制时间较长文件较大的情况）
          this.videoChunks2DataUrl([e.data]).then((videoData) => {
            console.log('fileReader onloadend:', videoData)
            if (typeof Buffer !== 'undefined' && this.options.dataType === 'buffer') {
              // 去除头的base64
              videoData = videoData.replace(/^data:video\/\w+;base64,/, '')
              videoData = Buffer.from(videoData, 'base64')
            }
            this.$emit('recordChunk', videoData, this.videoChunks.length)
          })
        }
      }
      // 结束
      this.videoRecorder.onstop = (e) => {
        console.log('videoRecorder onstop:', e)
        if (this.videoChunks.length > 1) return Promise.reject('为避免因整体视频过大而导致页面崩溃，分片视频请使用recordChunk事件')
        // 录制结束后，处理chunks，生成文件
        this.videoChunks2DataUrl(this.videoChunks).then((videoData) => {
          console.log('fileReader onloadend:', videoData)
          if (typeof Buffer !== 'undefined' && this.options.dataType === 'buffer') {
            // 去除头的base64
            videoData = videoData.replace(/^data:video\/\w+;base64,/, '')
            videoData = Buffer.from(videoData, 'base64')
          }
          this.$emit('recordEnd', videoData)
        })
      }
      // 暂停
      this.videoRecorder.onpause = (e) => {
        console.log('videoRecorder onpause:', e)
      }
      // 恢复
      this.videoRecorder.onresume = (e) => {
        console.log('videoRecorder onresume:', e)
      }
      // 监听错误
      this.videoRecorder.onerror = (err) => {
        console.log('videoRecorder onerror:', err)
        this.isShooting = false
      }
    },
    // 缩放
    makeScale (scale) {
      this.scale = Math.max(20, this.scale + scale)
    },
    // 拍照·方法1
    makePhoto () {
      if (!this.canMakePhoto || this.isShooting) return
      this.canMakePhoto = false
      let videoWrapper = this.$refs.videoWrapper
      let video = this.$refs.video
      let canvas = document.createElement('canvas')
      canvas.width = video.clientWidth
      canvas.height = video.clientHeight
      canvas.style.cssText = `
        width: ${video.clientWidth}px;
        height: ${video.clientHeight}px;
        position: absolute;
        left: ${video.offsetLeft}px;
        top: ${video.offsetTop}px;
        z-index: 3;
        box-shadow: inset 0 0 100px #999;
        opacity: 1;
        transition: all 1s ease;
      `
      videoWrapper.appendChild(canvas)
      let ctx = canvas.getContext('2d')
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
      canvas.addEventListener('transitionend', (e) => {
        if (e.propertyName !== 'width') return
        let photoData = canvas.toDataURL('image/jpg')
        if (typeof Buffer !== 'undefined' && this.options.dataType === 'buffer') {
          // 去除头的base64
          photoData = photoData.replace(/^data:image\/\w+;base64,/, '')
          photoData = Buffer.from(photoData, 'base64')
        }
        this.canMakePhoto = true
        videoWrapper.removeChild(canvas)
        this.$emit('photoEnd', photoData)
      })
      this.$nextTick(() => {
        // canvas.style.left = `${videoWrapper.clientWidth * 0.5}px`
        // canvas.style.top = `${videoWrapper.clientHeight}px`
        canvas.style.left = '50%'
        canvas.style.top = '100%'
        canvas.style.width = '0px'
        canvas.style.height = '0px'
        canvas.style.opacity = 0
      })
    },
    // 拍照·方法2
    makePhoto2 () {
      if (!this.canMakePhoto || this.isShooting) return
      let track = null
      if (this.mediaStream) {
        track = this.mediaStream.getTracks()[0]
      }
      if (!track) return
      this.canMakePhoto = false
      let imageCapture = new ImageCapture(track)
      imageCapture.takePhoto().then((blob) => {
        this.canMakePhoto = true
        let fileReader = new FileReader()
        fileReader.readAsDataURL(blob)
        fileReader.onloadend = (e) => {
          this.$emit('photoEnd', e.target.result)
        }
        fileReader.onerror = (err) => {
          console.log(err)
        }
      })
    },
    // 摄像
    makeShoot () {
      this.isShooting = !this.isShooting
      if (this.isShooting) this.makeShootStart()
      else this.makeShootEnd()
    },
    // 摄像·开始
    makeShootStart () {
      if (this.videoRecorder) {
        if (this.options.timeslice) this.videoRecorder.start(this.options.timeslice)
        else this.videoRecorder.start()
      }
      this.recordTimer = setInterval(() => {
        this.recordDuration++
      }, 1000)
    },
    // 摄像·结束
    makeShootEnd () {
      this.videoRecorder && this.videoRecorder.stop()
      this.recordTimer = clearInterval(this.recordTimer)
      this.recordDuration = 0
    },
    // 播放事件·回调
    playCallback () {
      this.error = false
    },
    // 当媒介能够开始播放时运行脚本·回调
    canplayCallback () {
      this.error = false
    },
    // 当在元素加载期间发生错误时运行脚本·回调
    errorCallback () {
      this.error = true
    },
    // 重置数据
    resetData () {
      this.mediaDeviceActiveIndex = 0
      this.constraints = { video: {}, audio: true }
      this.scale = 100
      this.isSaving = false
      this.isShooting = false
      this.error = false
      this.recordDuration = 0
      this.videoChunks = []
      clearInterval(this.recordTimer)
    }
  }
}
</script>
